package javango.contrib.hibernate;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.criterion.Expression;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.Type;

import com.google.inject.Inject;

public class ModelDaoHelper {
	private final static Log log = LogFactory.getLog(ModelDaoHelper.class);
	
	protected HibernateUtil hibernateUtil;
	protected Class<?> modelClass;
	
	public ModelDaoHelper(HibernateUtil hibernateUtil, Class modelClass) {
		this.hibernateUtil = hibernateUtil;
		this.modelClass = modelClass;
	}


	public java.lang.reflect.Field findField(Class modelClass, String property) throws NoSuchFieldException {
		// TODO dry,  this is repeated in ModelForm,  maybe helper/util class..
		Class classToTry = modelClass;
		while(classToTry != null) {
			try {
				return classToTry.getDeclaredField(property);
			} catch (NoSuchFieldException e) {
				classToTry = classToTry.getSuperclass();
			}
		}
		return modelClass.getDeclaredField(property); // let this throw the excpetion,  i guess we could do so ourselves...
	}
	
	private ConvertUtilsBean convertUtilsBean = new ConvertUtilsBean();
	public Object convert(String value, Class clazz) {		
		return convertUtilsBean.convert(value, clazz);
	}
	
	public Object correctType(String fieldName, Object value) throws DaoException {
		return correctType(fieldName, value, this.modelClass);
	}
	
	public Object correctType(String fieldName, Object value, Class clazz) throws DaoException {
		
		try {
			// TODO May still need to convert non-strings to the correct value (ie long to double, etc).
			if (!(value instanceof String)) {
				return value;
			}		
			ClassMetadata metadata = hibernateUtil.getSessionFactory().getClassMetadata(clazz);
			if (metadata != null) {
				Type type = metadata.getPropertyType(fieldName);
				if (type instanceof CollectionType) {
					log.warn("Unable to handle collection: " + fieldName);
					return value;
				} else if (type instanceof AssociationType) {
					// log.warn("Unable to handle association: " + fieldName);
					AssociationType assocType = (AssociationType)type;
					Class assocClass = assocType.getReturnedClass();
					ModelDao<?> assocDao = new ModelDao(hibernateUtil, assocClass);
					Class[] assocPkClass = assocDao.getPkClass();
					if (assocPkClass.length > 1) {
						log.error("Multi keyed associations are not supported in correctType");
						return value;
					}
					Object key = convert((String)value, assocPkClass[0]);
					return assocDao.get((Serializable)key);
					// TODO if return value is null should something be done??
					//return value;
				}				
			}
				
			java.lang.reflect.Field field = findField(clazz, fieldName);
			return convert((String)value, field.getType());

		} catch (NoSuchFieldException e) {
			log.error(e,e);
			throw new DaoException(e);
		}
	}
	
	public void processField(Criteria criteria, String fieldName, Object value, String alias, Class aliasClass) throws DaoException {

		int qualifierLocation = fieldName.indexOf("__");
		if (qualifierLocation == -1) {
			criteria.add(Expression.eq(fieldName,  correctType(fieldName, value)));
			return;
		}
		String qualifier = fieldName.substring(qualifierLocation+2);
		fieldName = fieldName.substring(0,qualifierLocation);

		Object searchValue = null;
		if (alias != null) {			
			searchValue = correctType(fieldName, value, aliasClass);
			fieldName = alias + "." + fieldName;
		} else {
			searchValue = correctType(fieldName, value);
		}
		
		 
		
		if ("like".equals(qualifier)) {
			criteria.add(Expression.like(fieldName, searchValue));
		} else if ("lte".equals(qualifier)) {
			criteria.add(Expression.le(fieldName, searchValue));
		} else if ("gte".equals(qualifier)) {
			criteria.add(Expression.ge(fieldName, searchValue));
		} else if ("eq".equals(qualifier)) {
			criteria.add(Expression.eq(fieldName, searchValue));
		} else if ("ne".equals(qualifier)) {
			criteria.add(Expression.ne(fieldName, searchValue));
		} else {
			throw new DaoException ("Unknown search type '" + qualifier + "'");
		}
	}
	
	public void updateCriteriaFromMap(Criteria criteria, Map<String, Object> params, String alias, Class aliasClass) throws DaoException {
		if (params == null) return;
		
		for(Entry<String, Object> entry : params.entrySet()) {
			if (entry.getValue() != null) processField(criteria, entry.getKey(), entry.getValue(), alias, aliasClass);
		}
	}
	
	public void updateCriteria(Criteria criteria, Object params) throws DaoException {
		updateCriteria(criteria, params, null, null);		
	}
	
	@SuppressWarnings("unchecked")
	public void updateCriteria(Criteria criteria, Object params, String alias, Class aliasClass) throws DaoException {
		if (alias != null) {
			criteria.createAlias(alias, alias);
		}
		
		if (params == null) return;
		
		if (params instanceof Map) {
			updateCriteriaFromMap(criteria, (Map)params, alias, aliasClass);
			return;			
		}
		
		try {
			Class c = params.getClass();
			java.lang.reflect.Field[] fields = c.getDeclaredFields();
			for (int i=0; i<fields.length; i++) {
				String fieldName = fields[i].getName();
				Object value = PropertyUtils.getProperty(params, fieldName);
				if (value != null) processField(criteria, fieldName, value, alias, aliasClass);
			}			
		} catch (IllegalAccessException e) {
			log.error(e,e);
		} catch (InvocationTargetException e) {
			log.error(e,e);
		} catch (NoSuchMethodException e) {
			log.error(e,e);
		}	
	}
}
